本文主要讨论和介绍Swift的并发编程,现在iOS中有两套比较先进的并发编程方案可以供我们使用,一个是GCD,另一个是操作队列(Operation Queues)。GCD是一套基于C的底层API,而操作队列是在GCD基础上的面相对象抽象,GCD提供了更加底层的控制,而操作队列则在GCD之上实现了一些方便的功能。


Operations & Operation Queues

Operations(操作)以一种面向对象的方式来封装需要执行的异步任务,是一套较为高级的API。Operations在Swift中主要有OperationBlockOperation两个类,他们可以各自单独使用,也可以被添加到Operation Queue(操作队列)中执行任务。

Operations

Operation

Operation是操作的一个基类,我们一般可以通过继承的方式来实现一些自定义的操作类,例如通过自定义操作类我们能够更改操作的执行方式或者打印作业执行过程中的状态变化。如果我们直接继承Operation实现的操作类是同步(Sync)执行的,我们也可以通过手动管理state来实现操作类的异步(Async)执行。下面我们将自定义实现一个AsyncOperation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class AsyncOperation: Operation {
enum State: String {
case Ready, Executing, Finished

fileprivate var keyPath: String {
return "is" + rawValue
}
}

var state = State.Ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
}

extension AsyncOperation {
// NSOperation Overrides
override var isReady: Bool {
return super.isReady && state == .Ready
}

override var isExecuting: Bool {
return state == .Executing
}

override var isFinished: Bool {
return state == .Finished
}

override var isAsynchronous: Bool {
return true
}

override func start() {
if isCancelled {
state = .Finished
return
}
main()
state = .Executing
}

override func cancel() {
state = .Finished
}
}

实现了AsyncOperation之后,我们可以通过以下步骤调用:

1.继承AsyncOperation
2.重写main方法,在main方法中执行Async操作
3.在Async的callback方法中将state置为.Finished

调用示例:

1
2
3
4
5
6
7
8
9
10
class MyLoginOperation: AsyncOperation {
override func main() {
RequestAPI.login(params, callback: {
self.state = .Finished
// TODO: login success
})
}
}
let op = MyLoginOperation()
OperationQueue().addOperation(op)

BlockOperation

BlockOperation,继承自Operation,可以并发的执行一个或多个block,只有当所有的block都执行完毕,整个操作才算执行完毕。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let blockOperation = BlockOperation {
sleep(2)
print("🍎")
}
blockOperation.addExecutionBlock {
print("🍏")
}
blockOperation.addExecutionBlock {
print("🍐")
}
blockOperation.addExecutionBlock {
print("🍇")
}
blockOperation.completionBlock = {
print("all works cmplete!")
}
blockOperation.start()

示例输出:

1
2
3
4
5
🍇
🍏
🍐
🍎
all works cmplete!

Operation Queues

上面我们提到了我们可以调用operation自身的start方法出发作业执行,但是在日常开发中,我们往往采用将operation添加到一个指定的queue中来执行作业。操作队列是OperationQueue的一个实例,任务一旦被添加到操作队列中不久便会自动执行操作,所以如果需要设置操作或队列的一些属性,需要在其被添加到队列之前设置,在添加到队列之后的设置将不会生效。操作队列默认是一个并行队列,我们可以通过maxConcurrentOperationCount这个属性设置队列的并发数,当并发数为1的时候,操作队列就是一个串行队列。操作队列默认是(Async)异步执行的,我们也可以通过调用waitUntilAllOperationsAreFinished()方法或将addOperations(_ ops: [Operation], waitUntilFinished wait: Bool)方法的waitUntilFinished属性设置为true,使其成为同步(Sync)同步队列。下面通过示例代码来详细讲解操作队列的使用。

1.首先我们创建了4个operation和一个操作队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let op1 = BlockOperation {
sleep(2)
print("🍎")
}
let op2 = BlockOperation {
print("🍏")
}
let op3 = BlockOperation {
print("🍐")
}
let op4 = BlockOperation {
print("🍇")
}

let queue = OperationQueue()

2.然后我们设置了队列的并发数为4,并将4个操作对象添加到了操作队列中:

1
2
queue.maxConcurrentOperationCount = 4
queue.addOperations([op1,op2,op3,op4], waitUntilFinished: true)

示例输出1:

1
2
3
4
🍏
🍐
🍇
🍎

这个结果我们并不感到意外,由于是并发队列,并且op1的作业中含有延时操作,所以前三个的输出顺序并不一定,但是第四个输出的一定是🍎。如果我们把上面第2步的代码修改如下:

1
2
queue.maxConcurrentOperationCount = 4
queue.addOperations([op1,op2,op3,op4], waitUntilFinished: false)

示例输出2:

1
2
3
🍏
🍐
🍇

此时队列并发异步执行操作,由于op1的延时处理,op1的执行将被直接返回,所以最终输出结果中并没有op1的输出。如果将上面的代码修改如下:

1
2
queue.maxConcurrentOperationCount = 1
queue.addOperations([op1,op2,op3,op4], waitUntilFinished: true)

示例输出3:

1
2
3
4
🍎
🍏
🍐
🍇

此时操作队列是一个串行队列,所以程序执行2s后依次执行队列中的操作,按FIFO的顺序输出了结果。如果我们将上面的代码修改为如下:

1
2
queue.maxConcurrentOperationCount = 1
queue.addOperations([op1,op2,op3,op4], waitUntilFinished: false)

示例输出4:

1
 

是的,代码执行后控制台将没有任何输出。由于我们设置队列的并发数为1,所以当前只有一个线程用于任务调度,同时我们设置了waitUntilFinished为false,说明这是一个异步执行的队列,所以当执行op1延时函数时,唯一的线程直接返回了,所以后续的操作都不会被执行。

Dependencies

如果在开发过程中我们想要操作按我们指定的顺序来执行,operation为我们提供了一种十分便捷的方式,operation支持互相设置依赖,如op1依赖于op2,op2依赖于op3,那么操作的执行顺秀就会是op3->op2->op1。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let op1 = BlockOperation {
print("🍎")
}
let op2 = BlockOperation {
print("🍏")
}
let op3 = BlockOperation {
print("🍐")
}

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
op1.addDependency(op2)
op2.addDependency(op3)
queue.addOperations([op1,op2,op3], waitUntilFinished: false)

示例输出:

1
2
3
🍐
🍏
🍎

Suspending and Resuming Queues

另外操作队列可以非常方便的进行挂起和恢复操作,我们可以通过队列的isSuspended属性设置队列的挂起和恢复,但是队列的挂起,不会影响已经被添加到队列中的操作,只有后续被添加到队列的操作会收到影响。我们直接看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let op1 = BlockOperation {
print("🍎")
}
let op2 = BlockOperation {
print("🍏")
}
let op3 = BlockOperation {
print("🍐")
}

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.addOperations([op1,op2], waitUntilFinished: false)

queue.isSuspended = true
queue.addOperation(op3)

示例输出:

1
2
🍎
🍏

Practice

在实际开发过程中,我们常常将一些耗时操作放到子线程,待操作完成后,我们切回main线程,进行一些UI相关的操作。其实这一过程可以用操作队列很方便的实现。

1
2
3
4
5
6
OperationQueue().addOperation {
// TODO: some time consuming operations
OperationQueue.main.addOperation {
// TODO: some UI operations
}
}

Operations小结

通过上面的介绍和使用,我们发现操作队列还是比较灵活和方便的,可以很方便的添加依赖。虽然它是高层级的API,相对于底层API来说可控制的权限会少一些,性能上可能稍有偏差,但在一定的场景下,还是很值得推荐的。


GCD

GCD是Apple提供的另一套基于C的高性能底层API,利用它我们可以进行高性能的并发编程。Swift3之前它还保持了C语言的调用分格,Swift3之后Apple对其进行了重新封装,新的API更符合面向对象思维,所有的任务都会被包装到一个函数或者闭包中,可读性更强,也更便于使用了。

Dispatch queues

Dispatch queues(调度队列)可以很方便的创建并执行(Async)异步和(Sync)同步任务。例如你可以将一些耗时任务添加到一个函数或者block,然后将其添加到调度队列中执行。GCD默认实现了一些队列,我们也可以通过GCD提供的方法创建自定义队列。

串行队列

在GCD中如不进行特殊处理默认创建的都是串行队列。用代码说话,我们可以通过如下方式创建串行队列:

1
let queue1 = DispatchQueue(label: "SerialQueue1")

向队列中添加作业可以通过如下方式,同时你还可以控制队列中的任务将以(Async)异步或(Sync)同步的方式执行。

1
2
3
4
5
6
7
8
9
10
11
12
queue1.sync {
print("🍊 \(Thread.current)")
}

queue1.async {
sleep(2)
print("🍎 \(Thread.current)")
}

queue1.sync {
print("🍐 \(Thread.current)")
}

示例输出:

1
2
3
🍊 <NSThread: 0x100a061e0>{number = 1, name = main}
🍎 <NSThread: 0x100a0b410>{number = 2, name = (null)}
🍐 <NSThread: 0x100a061e0>{number = 1, name = main}

调度队列中的main主队列就是一个串行队列,它只能存在于主线程之中。所以我们不能够往主队列中添加(Sync)同步操作,因为同步操作会堵塞主线程。

并发队列

系统默认实现了一个全局的并发队列,如果没有什么特殊需求一般我们直接可以拿来使用,示例如下:

1
2
3
4
5
6
DispatchQueue.global().async {
// TODO: some time consuming operations
DispatchQueue.main.async {
// TODO: some UI operations
}
}

这是我们日常开发中常用的一种方式,我们一般将比较耗时的操作放入全局并发队列,待耗时任务完成切换回主队列进行UI相关的操作。如果我们有特殊需求,同样可以自定义并发队列:

1
let queue = DispatchQueue(label: "concurrencyQueue", attributes: .concurrent)

Dispatch Group

如果在实际开发中我们希望多个网络请求都返回了在执行相应的操作,我们可以使用Dispatch Group很方便的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let group = DispatchGroup()
let queue = DispatchQueue(label: "groupQueue")

queue.async(group: group) {
print("🍏")
}

queue.async(group: group) {
print("🍎")
}

group.notify(queue: queue) {
print("all tasks complete")
}

示例输出:

1
2
3
🍏
🍎
all tasks complete

延时操作

在开发中我们经常会遇到一些延时操作,Dispatch queue提供了一套非常简单的方法。示例如下:

1
2
3
4
let queue = DispatchQueue(label: "queuename")
queue.asyncAfter(deadline: .now() + 2) {
// TODO: some task
}

GCD小结

本文列举了GCD在并发编程中常见的一些操作,还有一部分关于Dispatch Sources的没有展开,希望感兴趣的朋友可以自行研究。


操作队列(Operation Queues)还是GCD?

我认为在并发编程中,在相同的场景下,如果实现难度相当,我们首选GCD,毕竟它是比较底层的实现,性能也会出色些。但是如果有的场景操作队列实现起来方便得多,我们完全可以直接使用操作队列实现。另外,选择操作队列还是GCD也因人而异,不管是哪一种只要使用起来得心应手,都是可以的。